﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.Routing;

namespace TotalFreedomKO
{
    public enum KoExpressionType
    {
        Any = 0,
        ProperyOnly = 1,
    }

    public static class KoHelperExtensions
    {
        public static string[] KO_VISIBILITY_ATTRIBUTES = new string[] { "visible", "if" };
        public static string[] KO_TEXT_VISIBILITY_ATTRIBUTES = new string[] { "text", "visible", "if" };

        private static int _uniqueId = 0;

        #region control flow

        public static IKoHelper<TModel> Ko<TModel>(this HtmlHelper<TModel> htmlHelper)
        {
            KoHelper<TModel> koHelper = new KoHelper<TModel>(htmlHelper);

            return koHelper;
        }

        public static KoPropertySection<TModel, TProperty> KoGenericEnumerableSection<TModel, TProperty>(this IKoHelper<TModel> koHelper, Expression<Func<TModel, IEnumerable<TProperty>>> expression, string sectionName, KoExpressionType koExpressionType)
        {
            if (koHelper == null || koHelper.HtmlHelper == null)
                return null;

            string expr = "";

            if (expression != null)
            {
                PropertyData data = KoHelperExtensions.GetPropertyData(expression, koExpressionType == KoExpressionType.ProperyOnly);
                if (data != null && !String.IsNullOrEmpty(data.Expression))
                    expr = HttpUtility.HtmlEncode(data.Expression);
            }

            string s = "<!-- ko " + sectionName + ": " + expr + " -->";

            koHelper.HtmlHelper.ViewContext.Writer.Write(s);

            return new KoPropertySection<TModel, TProperty>(koHelper.HtmlHelper);
        }

        public static KoPropertySection<TModel, TProperty> KoGenericPropertySection<TModel, TProperty>(this IKoHelper<TModel> koHelper, Expression<Func<TModel, TProperty>> expression, string sectionName, KoExpressionType koExpressionType)
        {
            if (koHelper == null || koHelper.HtmlHelper == null)
                return null;

            string expr = "";

            if (expression != null)
            {
                PropertyData data = KoHelperExtensions.GetPropertyData(expression, koExpressionType == KoExpressionType.ProperyOnly);
                if (data != null && !String.IsNullOrEmpty(data.Expression))
                    expr = HttpUtility.HtmlEncode(data.Expression);
            }

            string s = "<!-- ko " + sectionName + ": " + expr + " -->";

            koHelper.HtmlHelper.ViewContext.Writer.Write(s);

            return new KoPropertySection<TModel, TProperty>(koHelper.HtmlHelper);
        }

        public static KoPropertySection<TModel, TProperty> KoGenericPropertySection<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string sectionName, KoExpressionType koExpressionType)
        {
            if (htmlHelper == null)
                return null;

            string expr = "";

            if (expression != null)
            {
                PropertyData data = KoHelperExtensions.GetPropertyData(expression, koExpressionType == KoExpressionType.ProperyOnly);
                if (data != null && !String.IsNullOrEmpty(data.Expression))
                    expr = HttpUtility.HtmlEncode(data.Expression);
            }

            string s = "<!-- ko " + sectionName + ": " + expr + " -->";

            htmlHelper.ViewContext.Writer.Write(s);

            return new KoPropertySection<TModel, TProperty>(htmlHelper);
        }

        public static KoModelSection<TModel> KoGenericModelSection<TModel, TProperty>(this IKoHelper<TModel> koHelper, Expression<Func<TModel, TProperty>> expression, string sectionName, KoExpressionType koExpressionType)
        {
            if (koHelper == null || koHelper.HtmlHelper == null)
                return null;

            string expr = "";

            if (expression != null)
            {
                PropertyData data = KoHelperExtensions.GetPropertyData(expression, koExpressionType == KoExpressionType.ProperyOnly);
                if (data != null && !String.IsNullOrEmpty(data.Expression))
                    expr = HttpUtility.HtmlEncode(data.Expression);
            }

            string s = "<!-- ko " + sectionName + ": " + expr + " -->";

            koHelper.HtmlHelper.ViewContext.Writer.Write(s);

            return new KoModelSection<TModel>(koHelper.HtmlHelper);
        }

        public static KoSection KoGenericSection<TModel, TProperty>(this IKoHelper<TModel> koHelper, Expression<Func<TModel, TProperty>> expression, string sectionName, KoExpressionType koExpressionType)
        {
            if (koHelper == null || koHelper.HtmlHelper == null)
                return null;

            string expr = "";

            if (expression != null)
            {
                PropertyData data = KoHelperExtensions.GetPropertyData(expression, koExpressionType == KoExpressionType.ProperyOnly);
                if (data != null && !String.IsNullOrEmpty(data.Expression))
                    expr = HttpUtility.HtmlEncode(data.Expression);
            }

            string s = "<!-- ko " + sectionName + ": " + expr + " -->";

            koHelper.HtmlHelper.ViewContext.Writer.Write(s);

            return new KoSection(koHelper.HtmlHelper.ViewContext);
        }

        public static KoSection KoGenericSection<TModel, TProperty>(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string sectionName, KoExpressionType koExpressionType)
        {
            if (htmlHelper == null)
                return null;

            string expr = "";

            if (expression != null)
            {
                PropertyData data = KoHelperExtensions.GetPropertyData(expression, koExpressionType == KoExpressionType.ProperyOnly);
                if (data != null && !String.IsNullOrEmpty(data.Expression))
                    expr = HttpUtility.HtmlEncode(data.Expression);
            }

            string s = "<!-- ko " + sectionName + ": " + expr + " -->";

            htmlHelper.ViewContext.Writer.Write(s);

            return new KoSection(htmlHelper.ViewContext);
        }

        public static KoPropertySection<TModel, TProperty> KoForeach<TModel, TProperty>(this IKoHelper<TModel> koHelper, Expression<Func<TModel, IEnumerable<TProperty>>> expression)
        {
            return KoGenericEnumerableSection(koHelper, expression, "foreach", KoExpressionType.ProperyOnly);
        }

        public static KoSection KoIf<TModel, TProperty>(this IKoHelper<TModel> koHelper, Expression<Func<TModel, TProperty>> expression)
        {
            return KoGenericSection(koHelper, expression, "if", KoExpressionType.Any);            
        }

        public static KoSection KoIf<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
        {
            return KoGenericSection(htmlHelper, expression, "if", KoExpressionType.Any);
        }

        public static KoSection KoIfnot<TModel, TProperty>(this IKoHelper<TModel> koHelper, Expression<Func<TModel, TProperty>> expression)
        {
            return KoGenericSection(koHelper, expression, "ifnot", KoExpressionType.Any);
        }

        public static KoSection KoIfnot<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
        {
            return KoGenericSection(htmlHelper, expression, "ifnot", KoExpressionType.Any);
        }

        public static KoPropertySection<TModel, TProperty> KoWith<TModel, TProperty>(this IKoHelper<TModel> koHelper, Expression<Func<TModel, TProperty>> expression)
        {
            return KoGenericPropertySection(koHelper, expression, "with", KoExpressionType.Any);
        }

        public static KoPropertySection<TModel, TProperty> KoWith<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
        {
            return KoGenericPropertySection(htmlHelper, expression, "with", KoExpressionType.Any);
        }

        #endregion

        #region Controling text and appearance 

        public static MvcHtmlString KoText<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
        {
            if (htmlHelper == null)
                return null;

            string expr = "";

            if (expression != null)
            {
                PropertyData data = KoHelperExtensions.GetPropertyData(expression);
                if (data != null && !String.IsNullOrEmpty(data.Expression))
                    expr = HttpUtility.HtmlEncode(data.Expression);
            }

            string s = "<!-- ko text: " + expr + " --><!--/ko-->";
            
            return new MvcHtmlString(s);
        }

        #endregion

        public static IKoElementHtmlString<TModel> KoTemplate<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, string templateName, Expression<Func<TModel, TProperty>> expression = null, string divId = null, string htmlAttributes = null)
        {
            string openingElement = "<div ";
            if (!String.IsNullOrWhiteSpace(divId))
                openingElement += "id=\"" + HttpUtility.HtmlAttributeEncode(divId) + "\" ";

            openingElement += "template: { name: '" + HttpUtility.HtmlAttributeEncode(templateName) + "'";

            if (expression != null)
            {
                PropertyData data = KoHelperExtensions.GetPropertyData(expression, true);
                if (data != null && !String.IsNullOrEmpty(data.PropertyName))
                    openingElement += ", data: " + HttpUtility.HtmlAttributeEncode(data.PropertyName);
            }

            openingElement += "} ";

            string closingElement = "></div>";

            return new KoElementHtmlString<TModel, TProperty>(openingElement, closingElement, null, null, htmlHelper.ViewData, null);
        }

        public static IKoElementHtmlString<TModel> KoSpan<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression = null, string spanId = null, string htmlAttributes = null)
        {
            string openingElement = "<span ";
            if (!String.IsNullOrWhiteSpace(spanId))
                openingElement += "id=\"" + HttpUtility.HtmlAttributeEncode(spanId) + "\" ";

            string closingElement = "></span>";

            return new KoElementHtmlString<TModel, TProperty>(openingElement, closingElement, null, null, htmlHelper.ViewData, KO_TEXT_VISIBILITY_ATTRIBUTES);
        }

        public static IKoElementHtmlString<TModel> KoHtmlTag<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, string tagName, Expression<Func<TModel, TProperty>> expression = null, string tagId = null, string htmlAttributes = null)
        {
            string openingElement = "<" + tagName  + " ";
            if (!String.IsNullOrWhiteSpace(tagId))
                openingElement += "id=\"" + HttpUtility.HtmlAttributeEncode(tagId) + "\" ";

            string closingElement = "></" + tagName + ">";

            return new KoElementHtmlString<TModel, TProperty>(openingElement, closingElement, null, null, htmlHelper.ViewData, null);
        }

        public static IKoElementHtmlString<TModel> KOUpdateButton<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string text = "Update", string cssClass = null)
        {
            string openingElement = "<button ";
            string closingElement = " class=\"updateButton " + cssClass + "\">" + HttpUtility.HtmlEncode(text) + "</button>";
            string koAttributeValue = "click: update";

            return new KoElementHtmlString<TModel, TProperty>(openingElement, closingElement, "click", koAttributeValue, htmlHelper != null ? htmlHelper.ViewData : null);
        }

        public static IKoElementHtmlString<TModel> KOCheckBox<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string cssDivClass = null, string cssLabelClass = null)
        {
            if (htmlHelper == null)
                return null;

            ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
            PropertyData data = GetPropertyData(expression);

            string openingElement = "<div ";
            
            string middleElement = " class=\"" + cssDivClass + "\">";
            middleElement += "<div class=\"subtitle\">";

            middleElement += "<input id=\"_cb" + HttpUtility.HtmlEncode(data.PropertyName) + "\" ";

            string koAttributeValue = "checked: " + HttpUtility.HtmlEncode(data.PropertyName);
            
            string closingElement = " type = \"checkbox\" /> ";
            closingElement += "<label for=\"_cb" + HttpUtility.HtmlEncode(data.PropertyName) + "\"><span class=\"" + cssLabelClass + "\"> " + HttpUtility.HtmlEncode(!String.IsNullOrEmpty(metadata.DisplayName) ? metadata.DisplayName : data.PropertyName) + " </span></label>";
            closingElement += "</div>";

            if (!String.IsNullOrEmpty(data.Description))
                closingElement += "<div class=\"subtitlelegend\">" + HttpUtility.HtmlEncode(data.Description) + "</div>";

            closingElement += "</div>";

            return new KoElement3HtmlString<TModel, TProperty>(openingElement, middleElement, closingElement, KO_VISIBILITY_ATTRIBUTES, "checked", koAttributeValue, htmlHelper.ViewData);
        }

        public static IKoElementHtmlString<TModel> KORadioBox<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, Type typeOfEnum, string cssDivClass = null, string cssLabelClass = null)
        {
            string openingElement = "<div ";

            string closingElement = " >";

            NameValueCollection options = GetEnumDescriptions(typeOfEnum);

            for (int i = 0; i < options.Count; i++)
            {
                string[] v = options.GetValues(i);

                if (v != null && v.Length > 0)
                {
                    IKoElementHtmlString<TModel> s2 = KORadioBox(htmlHelper, expression, options.GetKey(i), v[0]);
                    closingElement += s2.ToHtmlString();
                }
            }

            closingElement += "</div>";

            return new KoElementHtmlString<TModel, TProperty>(openingElement, closingElement, null, null, htmlHelper.ViewData, KO_VISIBILITY_ATTRIBUTES);
        }

        public static IKoElementHtmlString<TModel> KORadioBox<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string value, string label = null, string cssDivClass = null, string cssLabelClass = null)
        {
            if (htmlHelper == null)
                return null;

            PropertyData data = GetPropertyData(expression);

            _uniqueId++;

            string openingElement = "<input type=\"radio\" name=\"" + data.PropertyName + "\" value=\"" + HttpUtility.HtmlEncode(value) + "\" id=\"_rb" + data.PropertyName + _uniqueId + "\"";

            string koAttributeValue = "checked: " + data.PropertyName;
            
            string closingElement = " />";
            if (!String.IsNullOrEmpty(label))
                closingElement += "<label for=\"_rb" + data.PropertyName + _uniqueId + "\">" + HttpUtility.HtmlEncode(label) + "</label>";

            return new KoElementHtmlString<TModel, TProperty>(openingElement, closingElement, "checked", koAttributeValue, htmlHelper.ViewData);
        }

        public static KoSection KOIfEquals<TModel, TProperty, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, TValue value)
        {
            string strValue = Convert.ToString(value);
            return htmlHelper.KOIfEquals(expression, strValue);
        }

        public static KoSection KOIfEquals<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string value)
        {
            if (value == null || value.Length == 0)
                return new KoSection(htmlHelper.ViewContext);

            string s = "<!-- ko if: " + htmlHelper.BuildKoConditionStringFor(expression, value) + " -->";

            htmlHelper.ViewContext.Writer.Write(s);

            return new KoSection(htmlHelper.ViewContext);
        }

        public static KoSection KOIfOneOf<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string[] oneOfValues)
        {
            if (oneOfValues == null || oneOfValues.Length == 0)
                return new KoSection(htmlHelper.ViewContext);


            string s = "<!-- ko if: ";
            s += htmlHelper.BuildKoConditionStringForOneOf(expression, oneOfValues);
            s += " -->";

            htmlHelper.ViewContext.Writer.Write(s);

            return new KoSection(htmlHelper.ViewContext);
        }

        public static string BuildKoConditionStringForOneOf<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string[] oneOfValues)
        {
            if (oneOfValues == null || oneOfValues.Length == 0)
                return "true";

            PropertyData data = GetPropertyData(expression);

            string s = "";
            string d = "";
            foreach (string v in oneOfValues)
            {
                s += d + data.PropertyName + "() == '" + HttpUtility.HtmlEncode(v) + "'";
                d = " || ";
            }

            return s;
        }

        public static IKoElementHtmlString<TModel> KOTextField<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string cssDivClass = null, string cssTextClass = null)
        {
            if (htmlHelper == null)
                return null;

            ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
            PropertyData data = GetPropertyData(expression);

            string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(data.PropertyName);

            string openingElement = "<div ";

            string middleElement = " class=\"" + cssDivClass + "\">";
            middleElement += "<div class=\"subtitle\">" + HttpUtility.HtmlEncode(!String.IsNullOrEmpty(metadata.DisplayName) ? metadata.DisplayName : data.PropertyName) + (metadata.IsRequired ? " (*)" : "") + "</div>";

            if (!String.IsNullOrEmpty(data.Description))
                middleElement += "<div class=\"subtitlelegend\">" + HttpUtility.HtmlEncode(data.Description) + "</div>";

            string valAttrs = "";
            IDictionary<string, object> validationAttributes = htmlHelper.GetUnobtrusiveValidationAttributes(data.PropertyName, metadata);
            if (validationAttributes != null)
            {
                foreach (var attr in validationAttributes)
                {
                    valAttrs += " " + attr.Key + "=\"" + HttpUtility.HtmlEncode(attr.Value.ToString()) + "\"";
                }
            }

            middleElement += "<input ";

            string koAttributeValue = "value: " + HttpUtility.HtmlEncode(data.PropertyName);
            
            string closingElement = " class = \"" + cssTextClass + "\"" + valAttrs + " name=\"" + HttpUtility.HtmlEncode(data.PropertyName) + "\" type=\"text\" /> ";
            closingElement += htmlHelper.ValidationMessageFor(expression);
            closingElement += "</div>";

            return new KoElement3HtmlString<TModel, TProperty>(openingElement, middleElement, closingElement, KO_VISIBILITY_ATTRIBUTES, "value", koAttributeValue, htmlHelper.ViewData);
        }

        private static NameValueCollection GetEnumDescriptions(Type typeOfEnum)
        {
            NameValueCollection result = new NameValueCollection();

            Array values = Enum.GetValues(typeOfEnum);

            if (values != null)
            {
                for (int i = 0; i < values.Length; i++)
                {
                    int intValue = (int)values.GetValue(i);
                    string stringValue = Enum.GetName(typeOfEnum, intValue);

                    result.Add(stringValue, GetEnumDescription(typeOfEnum, stringValue));
                }
            }

            return result;
        }

        private static NameValueCollection GetEnumDescriptionsWithIntKeys(Type typeOfEnum)
        {
            NameValueCollection result = new NameValueCollection();

            Array values = Enum.GetValues(typeOfEnum);

            if (values != null)
            {
                for (int i = 0; i < values.Length; i++)
                {
                    int intValue = (int)values.GetValue(i);
                    string stringValue = Enum.GetName(typeOfEnum, intValue);

                    result.Add(intValue.ToString(), GetEnumDescription(typeOfEnum, stringValue));
                }
            }

            return result;
        }
        public static string GetEnumDescription(Type typeOfEnum, string name)
        {
            FieldInfo fi = typeOfEnum.GetField(name);

            DescriptionAttribute[] attributes =
                (DescriptionAttribute[])fi.GetCustomAttributes(
                typeof(DescriptionAttribute),
                false);

            if (attributes != null &&
                attributes.Length > 0)
                return attributes[0].Description;
            else
                return name;
        }

        public static IKoElementHtmlString<TModel> KoSelect<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, NameValueCollection list, string optionsCaption = null, string cssDivClass = null, string cssSelectClass = null)
        {
            if (htmlHelper == null)
                return null;

            ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
            PropertyData data = GetPropertyData(expression);

            string openingElement = "<div class=\"" + cssDivClass + "\">";
            openingElement += "<div class=\"subtitle\">" + HttpUtility.HtmlEncode(!String.IsNullOrEmpty(metadata.DisplayName) ? metadata.DisplayName : data.PropertyName) + (metadata.IsRequired ? " (*)" : "") + "</div>";

            if (!String.IsNullOrEmpty(data.Description))
                openingElement += "<div class=\"subtitlelegend\">" + HttpUtility.HtmlEncode(data.Description) + "</div>";

            openingElement += "<select ";

            string koAttributeValue = "value: " + HttpUtility.HtmlEncode(data.PropertyName) + ", uniqueName: true";
            
            string closingElement = " class=\"" + cssSelectClass + "\">";

            if (list != null)
            {
                if (!String.IsNullOrEmpty(optionsCaption))
                    closingElement += "<option value=\"\">" + HttpUtility.HtmlEncode(optionsCaption) + "</option>";

                for (int i = 0; i < list.Count; i++)
                {
                    closingElement += "<option value=\"" + HttpUtility.HtmlEncode(list.GetKey(i)) + "\">" + HttpUtility.HtmlEncode(list.Get(i)) + "</option>";
                }
            }

            closingElement += "</select></div>";

            return new KoElementHtmlString<TModel, TProperty>(openingElement, closingElement, "value", koAttributeValue, htmlHelper.ViewData);
        }



        private static string GetLambda(System.Linq.Expressions.Expression expression, string propertyNamePrefix = null)
        {
            if (expression == null)
                return "";

            switch (expression.NodeType)
            {
                // for now no convert processing, let javascript take care of it
                case ExpressionType.Convert:
                    return GetLambda(((UnaryExpression)expression).Operand, propertyNamePrefix);

                case ExpressionType.MemberAccess:
                    MemberExpression memberExpression = (MemberExpression)expression;
                    return memberExpression.Member is PropertyInfo ? (propertyNamePrefix != null ? propertyNamePrefix : "") + memberExpression.Member.Name + "()" : null;

                case ExpressionType.ArrayLength:
                    return GetLambda(((UnaryExpression)expression).Operand, propertyNamePrefix) + ".length";

                case ExpressionType.Call:
                    MethodCallExpression methodExpr = (MethodCallExpression)expression;
                    return methodExpr.Method.Name + "()";

                case ExpressionType.Conditional:
                    ConditionalExpression condExpr = (ConditionalExpression)expression;
                    string test = GetLambda(condExpr.Test, propertyNamePrefix);
                    string ifTrue = GetLambda(condExpr.IfTrue, propertyNamePrefix);
                    string ifFalse = GetLambda(condExpr.IfFalse, propertyNamePrefix);
                    return " " +  test + " ? " + ifTrue + " : " + ifFalse + " ";

                case ExpressionType.Constant:
                    return GetJavaScriptConstant(((ConstantExpression)expression).Value);

                case ExpressionType.Not:
                case ExpressionType.Negate:
                    return "!" + GetLambda(((UnaryExpression)expression).Operand);

                case ExpressionType.OrElse:
                case ExpressionType.AndAlso:
                    {
                        BinaryExpression binaryExpr = (BinaryExpression)expression;

                        string left = GetLambda(binaryExpr.Left, propertyNamePrefix);
                        string right = GetLambda(binaryExpr.Right, propertyNamePrefix);
                        return "(" + left + (expression.NodeType == ExpressionType.OrElse ? " || " : " && ") + right + ")";
                    }

                case ExpressionType.Add:
                case ExpressionType.Divide:
                case ExpressionType.Multiply:
                case ExpressionType.Subtract:
                case ExpressionType.And:
                case ExpressionType.Or:
                case ExpressionType.GreaterThan:
                case ExpressionType.GreaterThanOrEqual:
                case ExpressionType.LessThan:
                case ExpressionType.LessThanOrEqual:
                case ExpressionType.NotEqual:
                case ExpressionType.Equal:
                    {
                        BinaryExpression binaryExpr = (BinaryExpression)expression;

                        string left = GetLambda(binaryExpr.Left, propertyNamePrefix);
                        string right = GetLambda(binaryExpr.Right, propertyNamePrefix);
                        string oper = GetOperand(expression.NodeType);


                        return left + " " + oper + " " + right;
                    }

                default:
                    throw (new Exception("Please implement this knockout feature"));
            }
        }

        private static string GetOperand(ExpressionType expressionType)
        {
            switch (expressionType)
            {
                case ExpressionType.AndAlso:
                    return "&&";

                case ExpressionType.And:
                    return "&";

                case ExpressionType.OrElse:
                    return "||";

                case ExpressionType.Or:
                    return "|";

                case ExpressionType.Equal:
                    return "==";

                case ExpressionType.NotEqual:
                    return "!=";

                case ExpressionType.Negate:
                    return "!";

                case ExpressionType.GreaterThan:
                    return ">";

                case ExpressionType.GreaterThanOrEqual:
                    return ">=";

                case ExpressionType.LessThan:
                    return "<";

                case ExpressionType.LessThanOrEqual:
                    return "<=";

                case ExpressionType.Add:
                    return " + ";

                case ExpressionType.Subtract:
                    return " - ";

                case ExpressionType.Multiply:
                    return " * ";

                case ExpressionType.Divide:
                    return " / ";

                default:
                    throw(new Exception("Please implement this knockout feature"));
            }
        }

        public static PropertyData GetPropertyData<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression, bool onlyProperty = false, string propertyNamePrefix = null)
        {
            PropertyData data = new PropertyData();


            if (expression.Body.NodeType == ExpressionType.MemberAccess)
            {
                MemberExpression memberExpression = (MemberExpression)expression.Body;
                data.PropertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : null;
                Type containerType = memberExpression.Expression.Type;
                PropertyDescriptorCollection coll = TypeDescriptor.GetProperties(containerType);
                PropertyDescriptor descr = coll[data.PropertyName];
                if (descr != null)
                {
                    AttributeCollection attributes = descr.Attributes;
                    if (attributes != null)
                    {
                        DescriptionAttribute descrAttribute = (DescriptionAttribute)attributes[typeof(DescriptionAttribute)];
                        if (descrAttribute != null)
                            data.Description = descrAttribute.Description;
                    }
                }
            }
            else
            {
                if(onlyProperty)
                {
                    throw(new NotSupportedException("Only property supported as lambda expression for the attribute"));
                }

                data.Expression = GetLambda(expression.Body, propertyNamePrefix);
            }

            return data;
        }

        private static string GetJavaScriptConstant(object value)
        {
            if (value == null)
                return "null";

            if(value is bool)
                return ((bool)value) ? "true" : "false";

            if (value is int)
                return "" + ((int)value);
            
            if (value is double)
                return "" + ((double)value);

            return "'" + value.ToString() + "'";
        }

        public static string BuildKoConditionStringFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string value)
        {
            PropertyData data = KoHelperExtensions.GetPropertyData(expression);

            string s = "";
            if (value != null)
            {
                s = data.PropertyName + "() == '" + HttpUtility.HtmlEncode(value) + "'";
            }
            else
            {
                s = data.PropertyName + "() == null";
            }

            return s;
        }

        public static IKoElementHtmlString<TModel> KoSelect<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, Type typeOfEnum, string optionsCaption = null, string cssDivClass = null, string cssSelectClass = null)
        {
            if (htmlHelper == null)
                return null;

            NameValueCollection options = GetEnumDescriptionsWithIntKeys(typeOfEnum);

            return KoSelect(htmlHelper, expression, options, optionsCaption, cssDivClass, cssSelectClass);
        }

        public static IKoElementHtmlString<TModel> KoSelect<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, string>> expression, Type typeOfEnum, string optionsCaption = null, string cssDivClass = null, string cssSelectClass = null)
        {
            if (htmlHelper == null)
                return null;

            NameValueCollection options = GetEnumDescriptions(typeOfEnum);

            return KoSelect(htmlHelper, expression, options, optionsCaption, cssDivClass, cssSelectClass);
        }

        private static object GetModelStateValue2<TModel>(HtmlHelper<TModel> htmlHelper, string key, Type destinationType)
        {
            ModelState modelState;
            if (htmlHelper.ViewData.ModelState.TryGetValue(key, out modelState))
            {
                if (modelState.Value != null)
                {
                    return modelState.Value.ConvertTo(destinationType, null /* culture */);
                }
            }
            return null;
        }


        public static IKoElementHtmlString<TModel> KOSimpleTextField<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string cssDivClass = null, string cssTextClass = null)
        {
            if (htmlHelper == null)
                return null;

            ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
            PropertyData data = KoHelperExtensions.GetPropertyData(expression);

            string openingElement = "<div class=\"" + cssDivClass + "\">";
            openingElement += "<input ";
            
            string koAttributeValue = "value: " + HttpUtility.HtmlEncode(data.PropertyName) + ", uniqueName: true\"";
            
            string closingElement = " class = \"" + cssTextClass + "" + (metadata.IsRequired ? " required" : "") + "\" type=\"text\"/>";
            closingElement += "</div>";

            return new KoElementHtmlString<TModel, TProperty>(openingElement, closingElement, "value", koAttributeValue, htmlHelper.ViewData);
        }

        #region helper methods

        public static IHtmlString ArrayPush<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, IEnumerable<TProperty>>> expression, TProperty value)
        {
            string s = "";

            PropertyData data = KoHelperExtensions.GetPropertyData(expression, true);
            if (data != null)
            {
                s = data.PropertyName + ".push(" + BaseKoViewModel.GetJSON(value) + ")";
            }

            return new MvcHtmlString(s);
        }

        #endregion
    }
}
